import sys
import time
import traceback
from itertools import count

import paramiko
from paramiko.ssh_exception import NoValidConnectionsError

from .command import SshCommand
from .config import SshConfig
from .sftp import SftpClient


class SshClient:
    @classmethod
    def connect(cls,
        ssh_config: SshConfig, *,
        initial_delay: float = 3,
        connect_timeout: float = 3,
        retry_interval: float = 3,
        retry_times: int = 20,
        verbose: bool = True
    ) -> 'SshClient':
        if verbose:
            print(f"Connecting to {ssh_config.url}", file=sys.stderr)

        ssh_client = paramiko.SSHClient()
        ssh_client.set_missing_host_key_policy(paramiko.WarningPolicy)

        time.sleep(initial_delay)
        for i in count():
            try:
                ssh_client.connect(
                    hostname=ssh_config.hostname,
                    port=ssh_config.port,
                    username=ssh_config.username,
                    password=ssh_config.password,
                    pkey=ssh_config.private_key,
                    key_filename=ssh_config.private_key_filepath,
                    timeout=connect_timeout)
                break
            except (TimeoutError, NoValidConnectionsError):
                if i >= retry_times:
                    raise
                traceback.print_exc(file=sys.stderr)
                time.sleep(retry_interval)
                if verbose:
                    print("Retrying...", file=sys.stderr)
                continue

        return cls(ssh_client)

    def __init__(self, base: paramiko.SSHClient):
        self._base = base

    def execute(self,
        command: str, *,
        attaching: bool = True,
        waiting: bool = True,
        raising: bool = True,
    ) -> SshCommand:
        transport = self._base.get_transport()
        channel = transport.open_session()
        command = SshCommand(channel, command)

        if attaching:
            command.attach_stdout()
            command.attach_stderr()

        command.execute()

        if waiting:
            command.wait(raise_error=raising)

        return command

    def open_sftp(self) -> SftpClient:
        return self._base.open_sftp()
